<?php

namespace App\Models\Cache;

use App\Common\Libs\ArrayHelper;
use App\Common\Libs\Json;
use App\Consts\ExceptionCode;
use App\Consts\GlobalCacheConst;
use APP\Consts\GlobalCounterConst;
use App\Exceptions\Error;

/**
 * Class Cache
 * @package App\Models\Cache
 * @see Redis
 */
class Cache
{

    /** @var $_cache */
    private static $_cache;

    /** @var Redis $cacheClient */
    private $cacheClient;

    /** @var $keyParams */
    private $keyParams;

    /**
     * get instance
     * @param array $keyParams
     * @return static
     */
    public static function getInstance($keyParams = []): self
    {
        if (self::$_cache === null) {
            self::$_cache = new self();
        }
        self::$_cache->cacheClient = new Redis();
        self::$_cache->keyParams = $keyParams;
        return self::$_cache;
    }

    /**
     * @param $cacheKey [缓存键名]
     * @param $keyName [要从缓存数据中取出某值]
     * @return mixed
     */
    public function getValueFromArray($cacheKey, $keyName)
    {
        $array = $this->get($cacheKey);
        if (!$array || (!is_array($array) && !is_object($array)) || !in_array($keyName, $array)) {
            return false;
        }

        $index = array_search($keyName, $array);
        if (is_object($array)) {
            return $array->{$index};
        }

        return $array[$index];
    }

    /**
     * 向緩存數組中添加值
     * @param $cacheKey
     * @param $value
     * @param float|int $durating
     * @return bool
     */
    public function setValueToArray($cacheKey, $value, $durating = GlobalCacheConst::DEFAULT_EXPIRED_AT)
    {
        if (!empty($array[$value])) {
            return true;
        }

        $array = $this->get($cacheKey);
        if (!$array) {
            $array = [];
        }

        $array[] = $value;

        return self::$_cache->cacheClient::set($cacheKey, $array, $durating);
    }

    /**
     * 删除数组缓存中删除某值
     * @param $cacheKey
     * @param object|array|string|int $value
     * @return mixed
     */
    public function deleteValueFromArray($cacheKey, $value)
    {
        $array = $this->get($cacheKey);
        if (!$array) {
            return true;
        }

        if (!is_array($value) && !is_object($value) && !in_array($value, $array)) {
            return true;
        }

        if (is_array($value) || is_object($value)) {
            foreach ($value as $item) {
                $index = array_search($value, $array);
                unset($array[$index]);
            }
        } else {
            $index = array_search($value, $array);
            unset($array[$index]);
        }

        return $this->set($cacheKey, $array);
    }

    /**
     * 缓存数组中的某些值步长 + N
     * @param string $cacheKey [缓存键]
     * @param array |string $keys [要增加的索引]
     * @param int $count [步长]
     * @param bool $incrementUpdate [是否增量更新，如原有值是1，步长为2，则更新后的值为3]
     * @return mixed
     */
    public function incrFromArray($cacheKey, $keys, $count = 1, $incrementUpdate = true)
    {
        $cacheData = $this->hGetAll($cacheKey);
        if (!$cacheData) {
            if (!is_array($keys)) {
                $cacheData[$keys] = $count;

                $cacheData[$keys] = $cacheData[$keys] < 0 ? 0 : $cacheData[$keys];
                return $this->hset($cacheKey, $cacheData);
            }

            // 是索引数组 [a,b]
            $isAssocArray = $this->_isAssocArray($keys);
            if ($isAssocArray) {
                foreach ($keys as $key) {
                    $cacheData[$key] = $count;
                    $cacheData[$key] = $cacheData[$key] < 0 ? 0 : $cacheData[$key];
                }

                return $this->hset($cacheKey, $cacheData);
            }

            // 是关联数组 ['a' => 1, 'b' => 2]
            foreach ($keys as $key => $countNum) {
                if (empty($cacheData[$key]) || !$incrementUpdate) {
                    $cacheData[$key] = $countNum;
                } else {
                    $cacheData[$key] += $countNum;
                }
                $countNum = $countNum < 0 ? 0 : $countNum;
                $cacheData[$key] = $countNum;
            }

            return $this->hset($cacheKey, $cacheData);
        }

        // 缓存有值，且为非数组类型
        if (!is_array($cacheData)) {
            return false;
        }

        // 缓存有值，且为数组类型

        // 要修改的值不是数组
        if (!is_array($keys)) {
            $cacheData = $this->_reassionCacheData($cacheData, $keys, $count, $incrementUpdate);

            return $this->hset($cacheKey, $cacheData);
        }

        // 要修改的值是索引数组
        $isAssocArray = $this->_isAssocArray($keys);
        if ($isAssocArray) {
            foreach ($keys as $key) {
                $cacheData = $this->_reassionCacheData($cacheData, $key, $count, $incrementUpdate);
            }

            return $this->hset($cacheKey, $cacheData);
        }

        // 要修改的值是关联数组
        foreach ($keys as $key => $countNum) {
            $cacheData = $this->_reassionCacheData($cacheData, $key, $countNum, $incrementUpdate);
        }

        return $this->hset($cacheKey, $cacheData);
    }

    /**
     * @param $cacheData
     * @param $keyName
     * @param $countNum
     * @param $incrementUpdate
     * @return mixed
     */
    private function _reassionCacheData($cacheData, $keyName, $countNum, $incrementUpdate)
    {
        if (empty($cacheData[$keyName]) || !$incrementUpdate) {
            $cacheData[$keyName] = $countNum;
        } else {
            $cacheData[$keyName] += $countNum;
        }

        $cacheData[$keyName] = $cacheData[$keyName] < 0 ? 0 : $cacheData[$keyName];
        return $cacheData;
    }

    /**
     * 判断缓存数据中的某值是否存在
     * @param $cacheKey
     * @param $value
     * @return mixed
     */
    public function judgeValueExist($cacheKey, $value = '', $isFromArray = false)
    {
        if ($isFromArray) {
            return $this->getValueFromArray($cacheKey, $value);
        }

        return $this->get($cacheKey);
    }

    /**
     * 填充缓存键中的变量
     * @param $cacheKey
     * @param $params
     * @return string
     */
    public function getCacheKeyByfillVariable($cacheKey, $params)
    {
        if (empty($params)) {
            return $cacheKey;
        }
        preg_match_all('/{\$(.*?)}/', $cacheKey, $match);
        foreach ($match[1] as $index => $value) {
            $cacheKey = str_replace($match[0][$index], $params[$value], $cacheKey);
        }

        return $cacheKey;
    }

    /**
     * 获取数组缓存的指定key值
     * @param string $cacheKey 缓存的key值
     * @param string|array $keys 缓存的数组内容的下标键，如： $cache = [ 'like' => 1] $key = like
     * @return int|mixed
     */
    public function getCounter($cacheKey, $keys)
    {
        $cacheData = self::get($cacheKey);
        if (is_array($keys)) {
            foreach ($keys as $key) {
                if (empty($cacheData[$key]) || $cacheData[$key] <= 0) {
                    $cacheData[$key] = 0;
                }
                $cacheData = $this->_getCounterCacheData($key, $cacheData);
            }
            return $cacheData;
        } else {
            $cacheData = $this->_getCounterCacheData($keys, $cacheData);
            if (empty($cacheData[$keys]) || $cacheData[$keys] <= 0) {
                return 0;
            }
            return $cacheData[$keys];
        }
    }

    /**
     * @param $key
     * @param $cacheData
     * @return mixed
     */
    private function _getCounterCacheData($key, &$cacheData)
    {
        if ($key == GlobalCounterConst::COUNT_COLLECT) {
            $fakeCollect = $cacheData[GlobalCounterConst::FAKE_COLLECT] ?? 0;
            $countNum = $cacheData[$key] ?? 0;
            $cacheData[$key] = $countNum + $fakeCollect;
        } else if ($key == GlobalCounterConst::COUNT_LIKE) {
            $fakeLike = $cacheData[GlobalCounterConst::FAKE_LIKE] ?? 0;
            $countNum = $cacheData[$key] ?? 0;
            $cacheData[$key] = $countNum + $fakeLike;
        } else if ($key == GlobalCounterConst::COUNT_USER_RECOMMEND) {
            $fakeLike = $cacheData[GlobalCounterConst::FAKE_RECOMMEND] ?? 0;
            $countNum = $cacheData[$key] ?? 0;
            $cacheData[$key] = $countNum + $fakeLike;
        }
        return $cacheData;
    }

    /**
     * set driver
     * @param $db
     * @return $this
     */
    public function setDriver($db)
    {
        if (empty($db)) $db = 'default';
        return $this;
    }

    /**
     * get cache driver
     * @param string $db
     * @return Redis|mixed
     */
    public function driver($db = '')
    {
        $trace = (new \Exception)->getTrace();
        if (!isset($trace[1]['function']) || !isset($trace[1]['args'][0])) {
            return $this->_throwError('21010 - function error.');
        }
        list($method, $cacheKey) = [$trace[1]['function'], $trace[1]['args'][0]];
        self::$_cache->method = $method;
        $this->checkKey($cacheKey, $method);
        $db && $this->setDriver($db);
        return self::$_cache->cacheClient;
    }

    /**
     * get cache key
     * @param $cacheKey
     * @param bool $full
     * @return string
     */
    public function getKey($cacheKey, $full = false)
    {
        $cacheKey = $this->getCacheKeyByfillVariable($cacheKey, $this->keyParams);
        if ($full) $cacheKey = env('APP_ENV') . ':' . $cacheKey;
        return $cacheKey;
    }

    /**
     * @param $cacheKey [键]
     * @return mixed
     */
    public function get($cacheKey)
    {
        $cacheKey = $this->getKey($cacheKey);
        return $this->driver()::get($cacheKey);
    }

    /**
     * 设置缓存
     * @param $cacheKey
     * @param $value
     * @param $expired
     * @return mixed
     */
    public function set($cacheKey, $value, $expired = GlobalCacheConst::DEFAULT_EXPIRED_AT)
    {
        $cacheKey = $this->getKey($cacheKey);
        $result = $this->driver()::set($cacheKey, $value);
        $this->driver()::expire($cacheKey, $expired);
        return $result;
    }

    /**
     * 不存在时才设置缓存
     * [!] 默认两分钟过期
     * @param $cacheKey
     * @param $value
     * @param $expired
     * @return mixed
     */
    public function setnx($cacheKey, $value, $expired = GlobalCacheConst::EXPIRED_SIXTY)
    {
        $cacheKey = $this->getKey($cacheKey);
        $result = $this->driver()::setnx($cacheKey, $value);
        $this->driver()::expire($cacheKey, $expired);
        return $result;
    }

    /**
     * 直接删除缓存
     * @param $cacheKey
     * @return mixed
     */
    public function delete($cacheKey)
    {
        $cacheKey = $this->getKey($cacheKey);
        return $this->driver()::del($cacheKey);
    }

    /**
     * 获取并删除缓存
     * @param $cacheKey
     * @return mixed
     */
    public function pull($cacheKey)
    {
        $cacheKey = $this->getKey($cacheKey);
        $result = $this->driver()::get($cacheKey);
        $this->driver()::del($cacheKey);
        return $result;
    }

    /**
     * 设置【hash】类型缓存
     * @param $cacheKey
     * @param $array
     * @param float|int $expired
     * @return bool
     */
    public function hset($cacheKey, $array, $expired = GlobalCacheConst::DEFAULT_EXPIRED_AT)
    {
        $cacheKey = $this->getKey($cacheKey);

        foreach ($array as $k => $item) {
            if (is_array($item)) {
                $array[$k] = Json::encode($item);
            }
        }

        $this->driver()::hMSet($cacheKey, $array);
        $this->driver()::expire($cacheKey, $expired);
        return true;
    }

    /**
     * 获取【hash】类型缓存值
     * @param $cacheKey
     * @return array|mixed
     */
    public function hGetAll($cacheKey)
    {
        $cacheKey = $this->getKey($cacheKey);
        $data = $this->driver()::hGetAll($cacheKey);
        foreach ($data as $k => $v) {
            $arr = Json::decode($v);
            if (is_object($arr)) {
                $data[$k] = ArrayHelper::toArray($arr);
            } elseif (is_array($arr)) {
                $data[$k] = $arr;
            }
        }
        return $data;
    }

    /***
     * check key
     * @param $cacheKey
     * @param $type
     * @return bool|\Illuminate\Http\JsonResponse
     */
    public function checkKey($cacheKey, $type)
    {
        switch (strtolower($type)) {
            case 'get':
            case 'delete':
                return true;
                break;
            case 'set':
            case 'setnx':
                if (0 !== strpos($cacheKey, 'string:')) {
                    return $this->_throwError('21011 - type error.');
                }
                break;
            case 'hget':
            case 'hset':
            case 'hgetall':
                if (0 !== strpos($cacheKey, 'hash:')) {
                    return $this->_throwError('21012 - type error.');
                }
                break;
            case 'sget':
            case 'sset':
            case 'incrFromArray':
                if (0 !== strpos($cacheKey, 'set:')) {
                    return $this->_throwError('21013 - type error.');
                }
                break;
            case 'lpush':
            case 'rpush':
            case 'lpop':
            case 'rpop':
            case 'lrange':
                if (0 !== strpos($cacheKey, 'list:')) {
                    return $this->_throwError('21014 - type error.');
                }
                break;
            case 'zget':
            case 'zset':
                if (0 !== strpos($cacheKey, 'zset:')) {
                    return $this->_throwError('21015 - type error.');
                }
                break;
            default :
                return $this->_throwError('21099 - type error.');
                break;
        }
        return true;
    }

    /**
     * 验证数组类型是否为索引数组
     * @param array $array
     * @return bool
     */
    private function _isAssocArray(array $array)
    {
        $index = 0;
        foreach (array_keys($array) as $key) {
            if ($index++ != $key) {
                return false;
            }
        }

        return true;
    }

    /**
     * throw error
     * @param string $result
     * @return \Illuminate\Http\JsonResponse
     */
    private function _throwError($result = '')
    {
        return Error::handle('', ExceptionCode::CODE_FAIL, $result, true);
    }

}
